%%% ------------------------------------------------------------------
%%% Licensed under the Apache License, Version 2.0 (the 'License');
%%%  you may not use this file except in compliance with the License.
%%%  You may obtain a copy of the License at
%%%
%%%      http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Copyright (c) 2017 qingchuwudi <bypf2009@vip.qq.com>
%%%
%%%  Unless required by applicable law or agreed to in writing, software
%%%  distributed under the License is distributed on an 'AS IS' BASIS,
%%%  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%%  See the License for the specific language governing permissions and
%%%  limitations under the License.
%%%
%%% @doc cahce server
%%% @author  qingchuwudi <'bypf2009@vip.qq.com'>
%%% @copyright 2017 qingchuwudi <bypf2009@vip.qq.com>
%%% @end
%%% created|changed : 2017-09-05 11:57
%%% coding : utf-8
%%% ------------------------------------------------------------------
-module(elcache_service).
-author("qingchuwudi").

-behaviour(gen_server).
-export([
    start_link/1
]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([
    add/2,
    clean/1,
    change_function/2
]).

-include("elcache.hrl").

%%%====================================================================
%%% API functions
%%%====================================================================
start_link(#elcache{} = Opts) ->
    gen_server:start_link(?MODULE, Opts, []).

-spec add(pid(), any()) -> ok.
add(Pid, Data) ->
    gen_server:cast(Pid, {add, Data}).

-spec clean(pid()) -> ok.
clean(Pid) ->
    gen_server:cast(Pid, clean).

-spec change_function(pid(), Fun) -> ok when
        Fun :: fun().
change_function(Pid, Fun) when is_function(Fun, 1) ->
    gen_server:cast(Pid, {change_function, Fun}).

%%%====================================================================
%%% Inner functions
%%%====================================================================
%% Returns Ref, sends event {timeout,Ref,Msg} after Time
%% to the (then) current state.
start_timer(Time) ->
    erlang:start_timer(Time, self(), transaction).

%% Returns the remaing time for the timer if Ref referred to
%% an active timer/send_event_after, false otherwise.
cancel_timer(Ref) ->
    case erlang:cancel_timer(Ref) of
    false ->
        receive {timeout, Ref, _} -> 0
        after 0 -> false
        end;
    RemainingTime ->
        RemainingTime
    end.

restart_timer(Time, Ref) ->
    cancel_timer(Ref),
    start_timer(Time).

check_transaction(#elcache{function = Fun, cache_list = Cache,
                     count = C, cache_count = Cc} = State) ->
    if
        C > Cc ->
            State;
        true ->
            cancel_timer(State#elcache.tref),
            case catch Fun(Cache) of
                {'EXIT', _Reason} ->
                    [_| T] = Cache,
                    TRef = start_timer(State#elcache.timeout),
                    State#elcache{cache_list = T, cache_count = Cc - 1,
                                  tref = TRef};
                _ ->
                    TRef = start_timer(State#elcache.timeout),
                    State#elcache{cache_list = [], cache_count = 0, tref = TRef}
            end
    end.

transaction(#elcache{function = Fun, cache_list = Cache} = State) ->
    case catch Fun(Cache) of
        {'EXIT', _Reason} ->
            TRef = start_timer(State#elcache.timeout),
            State#elcache{tref = TRef};
        _ ->
            TRef = start_timer(State#elcache.timeout),
            State#elcache{cache_list = [], cache_count = 0, tref = TRef}
    end.

%%%====================================================================
%%% Call-Back functions
%%%====================================================================

init(Opts) ->
    TRef = start_timer(Opts#elcache.timeout),
    {ok, Opts#elcache{tref = TRef}}.

handle_call(_Request, _From, State) ->
    {reply, {error, unknown_call}, State}.

handle_cast({add, Data}, #elcache{cache_count = C2,
                                  cache_list = Cache} = State) ->
    Cache1 = [Data | Cache],
    C3 = C2 + 1,
    State1 = State#elcache{cache_count = C3, cache_list = Cache1},
    State2 = check_transaction(State1),
    {noreply, State2};

handle_cast(clean, State) ->
    TRef = restart_timer(State#elcache.timeout,
                         State#elcache.tref),
    State1 = State#elcache{cache_count = 0, cache_list = [], tref = TRef},
    {noreply, State1};

handle_cast({change_function, Fun}, State) when is_function(Fun, 1) ->
    {noreply, State#elcache{function = Fun}};

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info({timeout, _Ref, transaction}, State) ->
    State1 = transaction(State),
    {noreply, State1};

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, State) ->
    check_transaction(State),
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.